const ExtensionsManager = require('./extensionsManager')
const DocumentStore = require('./documentStore')
const Templates = require('./templates')
const createLogger = require('./logger')
const runInSandbox = require('./sandbox/runInSandbox')
const createNoneEngine = require('./render/noneEngine')
const htmlRecipe = require('./render/htmlRecipe')
const defaultProxyExtend = require('./defaultProxyExtend')
const Reporter = require('../shared/reporter')
const BlobStorage = require('./blobStorage.js')
const Render = require('./render/render')
const Profiler = require('./render/profiler.js')
const engineStream = require('./render/engineStream.js')

class WorkerReporter extends Reporter {
  constructor (workerData, executeMain) {
    const { options, documentStore, extensionsDefs, workerId } = workerData

    super(options)

    this.workerId = workerId
    this._executeMain = executeMain
    this._initialized = false
    this._lockedDown = false
    this._documentStoreData = documentStore
    this._requestContextMetaConfigCollection = new Map()
    this._proxyRegistrationFns = []
    this.requestModulesCache = new Map()
    this._workerActions = new Map()
    this._registerRenderAction()

    this.registerHelpersListeners = this.createListenerCollection('registerHelpers')
    this.afterTemplatingEnginesExecutedListeners = this.createListenerCollection('afterTemplatingEnginesExecuted')
    this.validateRenderListeners = this.createListenerCollection('validateRender')

    this.extensionsManager = ExtensionsManager(this, extensionsDefs)

    this.extendProxy((proxy, req) => defaultProxyExtend(this)(proxy, req))
    this.beforeMainActionListeners = this.createListenerCollection('beforeMainAction')
  }

  async init () {
    if (this._initialized === true) {
      throw new Error('jsreport already initialized. Make sure init is called only once')
    }

    super.init()

    Templates(this)

    this.profiler = Profiler(this)
    this.logger = createLogger(this.profiler)

    this._render = Render(this)
    await this.extensionsManager.init()

    this.documentStore = DocumentStore(this._documentStoreData, this.executeMainAction.bind(this))
    this.blobStorage = BlobStorage(this.executeMainAction.bind(this), { writeTempFile: this.writeTempFile.bind(this), readTempFile: this.readTempFile.bind(this) })

    this.addRequestContextMetaConfig('rootId', { sandboxReadOnly: true })
    this.addRequestContextMetaConfig('id', { sandboxReadOnly: true })
    this.addRequestContextMetaConfig('reportCounter', { sandboxReadOnly: true })
    this.addRequestContextMetaConfig('startTimestamp', { sandboxReadOnly: true })
    this.addRequestContextMetaConfig('workerAllocatedTimestamp', { sandboxReadOnly: true })
    this.addRequestContextMetaConfig('logs', { sandboxReadOnly: true })
    this.addRequestContextMetaConfig('isChildRequest', { sandboxReadOnly: true })
    this.addRequestContextMetaConfig('originalInputDataIsEmpty', { sandboxReadOnly: true })
    this.addRequestContextMetaConfig('skipModificationDateUpdate', { sandboxHidden: true })

    this._runInSandbox = runInSandbox(this)

    const { compile: compileNone, execute: executeNone } = createNoneEngine()

    this.extensionsManager.engines.push({
      name: 'none',
      compile: compileNone,
      execute: executeNone
    })

    this.extensionsManager.recipes.push({
      name: 'html',
      execute: htmlRecipe
    })

    engineStream(this)

    await this.initializeListeners.fire()

    if (!this._lockedDown && this.options.trustUserCode === false) {
      require('@jsreport/ses')

      // eslint-disable-next-line
      repairIntrinsics({
        // don't change locale based methods which users may be using in their templates
        localeTaming: 'unsafe',
        errorTaming: 'unsafe',
        stackFiltering: 'verbose',
        /*
            FROM SES DOCS
            The 'severe' setting enables all the properties on at least Object.prototype, which is sometimes needed for compatibility with code generated by rollup or webpack.
            However, this extra compatibility comes at the price of a miserable debugging experience.

            We need this to make jsrender working, which overrides constructor.
            In case we need to put back default, we will need to fork jsrender and change the following line
            (Tag.prototype = compiledDef).constructor = compiledDef._ctr = Tag;
            x
            Tag.prototype = compiledDef
            compiledDef._ctr = Tag
            */
        overrideTaming: 'severe'
      })

      // NOTE: we need to add these overrides between repairIntrinsics and hardenIntrinsics
      // for them to be valid.
      // in this mode we alias the unsafe methods to safe ones
      Buffer.allocUnsafe = function allocUnsafe (size) {
        return Buffer.alloc(size)
      }

      Buffer.allocUnsafeSlow = function allocUnsafeSlow (size) {
        return Buffer.alloc(size)
      }

      // eslint-disable-next-line
      hardenIntrinsics()

      // we also harden Buffer because we expose it to sandbox
      // eslint-disable-next-line
      harden(Buffer)

      // we need to expose Intl to sandbox
      // eslint-disable-next-line
      harden(Intl)

      this._lockedDown = true
    }

    this._initialized = true
  }

  /**
   * @public
   */
  addRequestContextMetaConfig (property, options) {
    this._requestContextMetaConfigCollection.set(property, options)
  }

  /**
   * @public
   */
  getRequestContextMetaConfig (property) {
    if (property === undefined) {
      const all = {}

      for (const [key, value] of this._requestContextMetaConfigCollection.entries()) {
        all[key] = value
      }

      return all
    }

    return this._requestContextMetaConfigCollection.get(property)
  }

  extendProxy (registrationFn) {
    this._proxyRegistrationFns.push(registrationFn)
  }

  createProxy ({ req, runInSandbox, context, getTopLevelFunctions, sandboxRequire }) {
    const proxyInstance = {}
    for (const fn of this._proxyRegistrationFns) {
      fn(proxyInstance, req, {
        runInSandbox,
        context,
        getTopLevelFunctions,
        sandboxRequire
      })
    }
    return proxyInstance
  }

  async render (req, parentReq) {
    return await this._render(req, parentReq)
  }

  async executeMainAction (actionName, data, req) {
    await this.beforeMainActionListeners.fire(actionName, data, req)
    return this._executeMain(actionName, data, req)
  }

  async runInSandbox ({
    manager,
    context,
    userCode,
    initFn,
    executionFn,
    currentPath,
    onRequire,
    propertiesConfig,
    errorLineNumberOffset
  }, req) {
    // we flush before running code in sandbox because it can potentially
    // include code that blocks the whole process (like `while (true) {}`) and we
    // want to ensure that the batched messages are flushed before trying to execute the code
    await this.profiler.flush(req.context.rootId)

    return this._runInSandbox({
      manager,
      context,
      userCode,
      initFn,
      executionFn,
      currentPath,
      onRequire,
      propertiesConfig,
      errorLineNumberOffset
    }, req)
  }

  registerWorkerAction (actionName, fn) {
    this._workerActions.set(actionName, fn)
  }

  async executeWorkerAction (actionName, data, req) {
    const action = this._workerActions.get(actionName)
    if (!action) {
      throw new Error(`Worker action ${actionName} not registered`)
    }
    return action(data, req)
  }

  _registerRenderAction () {
    this.registerWorkerAction('render', async (data, req) => {
      const response = await this._render(req)
      return response.serialize()
    })
  }

  async close () {
    this.logger.debug('Closing jsreport worker')
    return this.closeListeners.fire()
  }
}

module.exports = WorkerReporter
